package devcken.hibernate.connection;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;

import org.hibernate.HibernateException;
import org.hibernate.cfg.Environment;
import org.hibernate.service.jdbc.connections.internal.C3P0ConnectionProvider;
import org.jasypt.encryption.pbe.PBEStringEncryptor;
import org.jasypt.exceptions.EncryptionInitializationException;
import org.jasypt.hibernate3.connectionprovider.ParameterNaming;
import org.jasypt.hibernate3.encryptor.HibernatePBEEncryptorRegistry;
import org.jasypt.properties.PropertyValueEncryptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Database Connection 실패 상황을 고려한 Connection Provider로 Connection Property에 대한 암호화를 지원한다.
 * @author Devcken
 */
public class FailoverSupportConnectionProvider extends C3P0ConnectionProvider
{
	private static final long serialVersionUID = -7175288046374026324L;

	private final static Logger logger = LoggerFactory.getLogger(FailoverSupportConnectionProvider.class);
	
	/**
	 * Failover 상황에 대비한 여분의 Connection String의 property key를 나타낸다.
	 */
	private final static String FAILOVER_URL_KEY = "hibernate.connection.url.failover";
	
	/**
	 * 접속 시도 최대 횟수를 나타내며, 기본 Connection String과 여분의 Connection String을 이용한 시도가 모두 이루어졌을 때를 1회의 시도로 취급한다.
	 * 단, 여분의 Connection String이 존재하지 않는 경우에는 기본 Connection String에 대한 접속 시도를 1회의 시도로 취급한다.
	 */
	private final static int CONNECTION_ATTEMPT_MAX_LIMIT = 3;
	
	/**
	 * Hibernate Property 목록으로 Database 접속에 필요한 정보를 담는다.
	 */
	@SuppressWarnings("rawtypes")
	private Map _properties;
	
	/**
	 * true인 경우, Connection이 실패하여 여분의 Connection String으로 전환된 나타내며, false인 경우, 기본 Connection String인 경우를 나타낸다.
	 * 기본 Connection String을 사용하더라도 이미 Failover 상태일 수도 있다.
	 */
	private boolean _failoverSwitch = false;
	
	/**
	 * Connection 시도가 실패한 횟수를 나타내며, 기본 Connection String과 여분의 Connection String을 이용한 시도 모두가 실패했을 때를 한 번의 실패로 취급한다.
	 * 단, 여분의 Connection String이 존재하지 않는 경우, 기본 Connection String을 이용한 접속에 대한 실패를 한 번의 실패로 취급한다.
	 */
	private int _failoverCount = 0;
	
	public FailoverSupportConnectionProvider()
	{
		super();
	}

	/**
	 * 주어진 Connection String과 계정 정보를 통해 데이터베이스와 연결을 설정한다.
	 * 연결 시, hibernate.connection.autocommit property를 통해 AutoCommit 모드를 설정한다.
	 */
	@SuppressWarnings("unchecked")
	@Override
	public Connection getConnection() throws SQLException
	{
		Connection connection = null;
		
		try
		{
			connection = super.getConnection();
			
			// 접속이 완료되었으므로 접속 시도 횟수를 초기화한다.
			this._failoverCount = 0;
		}
		catch (SQLException error)
		{
			logger.error(error.getMessage(), error);
			
			// 여분의 Connection String이 존재하지 않거나, 해당 예외에 대한 접속 시도가 여분의 Connection String에 대한 것인 경우, 시도 횟수를 1회 증가시킨다. 
			if (!this._properties.containsKey(FAILOVER_URL_KEY) || this._failoverSwitch)
			{
				this._failoverCount++;
				
				// 접속 시도 횟수(접속 실패 횟수)가 최대 접속 시도 한도를 넘어선 경우, 예외를 던진다.
				if (this._failoverCount > CONNECTION_ATTEMPT_MAX_LIMIT)
				{
					throw error;
				}
			}
			// 기본 Connection String 으로 시도한 접속이 실패한 경우, Connection String을 여분으로 교체한다.
			else if (!this._failoverSwitch)
			{
				logger.error("Primary connection string is not available. Connection string is replaced with a spare.");
				
				String spareUrl = this._properties.get(Environment.URL).toString();
				
				this._properties.put(Environment.URL, this._properties.get(FAILOVER_URL_KEY).toString());
				
				this._properties.put(FAILOVER_URL_KEY, spareUrl);
			}
			
			this._failoverSwitch = !this._failoverSwitch;
			
			connection = this.getConnection();
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return connection;
	}

	/**
	 * Hibernate Property를 이용해 Conneciton Provider를 초기화한다.
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void configure(@SuppressWarnings("rawtypes") Map properties) throws HibernateException
	{
		try
		{
			HibernatePBEEncryptorRegistry encryptorRegistry = HibernatePBEEncryptorRegistry.getInstance();
			
			final String encryptorRegisteredName = properties.get(ParameterNaming.ENCRYPTOR_REGISTERED_NAME).toString();
			final PBEStringEncryptor encryptor = encryptorRegistry.getPBEStringEncryptor(encryptorRegisteredName);
	
			if (encryptor == null)
			{
				throw new EncryptionInitializationException("No string encryptor registered for hibernate with name \"" + encryptorRegisteredName + "\"");
			}
	
			final String driver = properties.get(Environment.DRIVER).toString();
			final String url = properties.get(Environment.URL).toString();
			final String secondaryUrl = properties.get(FAILOVER_URL_KEY).toString();
			final String user = properties.get(Environment.USER).toString();
			final String password = properties.get(Environment.PASS).toString();
	
			if (PropertyValueEncryptionUtils.isEncryptedValue(driver))
			{
				properties.put(Environment.DRIVER, PropertyValueEncryptionUtils.decrypt(driver, encryptor));
			}
			
			if (PropertyValueEncryptionUtils.isEncryptedValue(url))
			{
				properties.put(Environment.URL, PropertyValueEncryptionUtils.decrypt(url, encryptor));
			}
			
			if (PropertyValueEncryptionUtils.isEncryptedValue(secondaryUrl))
			{
				properties.put(FAILOVER_URL_KEY, PropertyValueEncryptionUtils.decrypt(secondaryUrl, encryptor));
			}
			
			if (PropertyValueEncryptionUtils.isEncryptedValue(user))
			{
				properties.put(Environment.USER, PropertyValueEncryptionUtils.decrypt(user, encryptor));
			}
			
			if (PropertyValueEncryptionUtils.isEncryptedValue(password))
			{
				properties.put(Environment.PASS, PropertyValueEncryptionUtils.decrypt(password, encryptor));
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		super.configure(properties);
		
		this._properties = properties;
	}
}